﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Security.Cryptography;
using System.Xml;

using System.Collections;

using LorraineSec.Libraries.Sensors;
using LorraineSec.DatabaseLib.DataObjects;
using LorraineSec.MessageLib.Messages;
using NetworkManagement;

namespace LorraineSec.ServerApplication
{
    /// <summary>
    /// Handles various aspects of a client connection in the ServerApplication.
    /// </summary>
    internal class ServerConnection
    {
        #region Static Variables
        private static DatabaseLib.XmlDatabase DB;
        public static ServerApplication ServApp
        {
            get
            {
                return _sApp;
            }
            set
            {
                _sApp = value;
                DB = ServApp.DataBase;
            }
        }
        private static ServerApplication _sApp;
        #endregion

        #region Constant/Enum/Instance Variables
        /// <summary>
        /// An enumeration of the possible states of clients.
        /// </summary>
        public enum Status
        {
            UNKNOWN,
            NON_CLIENT,
            CONNECTED,
            AUTHORIZED,
            DISCONNECTED
        };

        public uint ClientID;
        public User AuthenticatedUser;
        public Usergroup AuthUserGroup;

        /// <summary>
        /// An object that manages the cryptographic keys and encryption/descryption of
        /// byte arrays.
        /// </summary>
        public CryptoManager crypto;

        /// <summary>
        /// Interfaces with the underlying socket to send and recieve messages.
        /// </summary>
        public MessageStreamInterface msgInt;

        /// <summary>
        /// Represents the current state of the client.
        /// </summary>
        public Status status;

        /// <summary>
        /// A dictionary of device descriptions Key-Indexed by the Compartment they belong to.
        /// </summary>
        public Dictionary<Compartment, List<Device>> Subscriptions;
        #endregion

        public ServerConnection()
        {
            Subscriptions = new Dictionary<Compartment, List<Device>>();
        }

        #region Message Handlers
        #region Key Transfer/Authentication
        /// <summary>
        /// Handles a KeyRequestMessage by sending the client a KeyResponseMessage containing
        /// the Server Applications public RSA key.
        /// </summary>
        /// <param name="rsa">The Public Key cryptographic service.</param>
        /// <param name="msg">The recieved KeyRequestMessage.</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_KeyRequest(RSACryptoServiceProvider rsa, KeyRequestMessage msg)
        {
            KeyResponseMessage krm = new KeyResponseMessage();
            krm.keyString = rsa.ToXmlString(false);
            krm.encode();
            return msgInt.sendMessage(krm);
        }

        /// <summary>
        /// Handles a SymKeyExchangeMessage by generating a symmetric key based on the values
        /// in the message. This key should be used to encrypt all further messages.
        /// </summary>
        /// <param name="rsa">The Public Key cryptographic service.</param>
        /// <param name="msg">The received SymKeyExchangeMessage.</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_SymKeyExchange(RSACryptoServiceProvider rsa, SymKeyExchangeMessage msg)
        {
            msg.decrypt(rsa);
            crypto = new CryptoManager(msg.symKey, msg.IV);
            return true;
        }

        /// <summary>
        /// Handles a AuthRequestMessage by cross checking the username and password with the
        /// database and responding accordingly with a AuthResponseMessage.
        /// </summary>
        /// <param name="msg">The received AuthRequestMessage</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_AuthRequest(AuthRequestMessage msg)
        {
            if (crypto != null)
            {
                //Decrypt the message
                msg.decrypt(crypto);
                AuthResponseMessage arm = new AuthResponseMessage();
                //Check if the username exists in the database
                User usr = DB.getUserByUsername(msg.Username);
                if (usr != null && msg.PasswordHash.Equals(usr.Password))
                {
                    AuthenticatedUser = usr;
                    AuthUserGroup = DB.getUsergroupById((int)AuthenticatedUser.UserGroup);
                    arm.UserID = usr.ID;
                    arm.LoginStatus = AuthResponseMessage.Status.APPROVED;
                    arm.SessionID = (byte)usr.UserGroup;//Change this later
                }
                else
                {
                    arm.LoginStatus = AuthResponseMessage.Status.DENIED;
                }
                return msgInt.sendMessage(arm);
            }
            else
            {
                return false;
            }
            
        }
        #endregion

        #region List Requests
        /// <summary>
        /// Handles a CompartmentListRequest by returning a list of the compartments that
        /// this authenticated user is allowed to view.
        /// </summary>
        /// <param name="msg">The CompartmentListRequest message</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_CompListRequest(CompartmentListRequest msg)
        {
            //Check if user has authenticated
            if (AuthenticatedUser != null)
            {
                CompartmentListResponse clr = new CompartmentListResponse();
                clr.Compartments = new List<CompartmentListResponse.Compartment>();
                //Generate a list of compartment the user has privileges to view
                foreach (Compartment cmp in AuthUserGroup.GetCompartments(DB))
                {
                    clr.Compartments.Add(cmp);
                }
                clr.encrypt(crypto);
                return msgInt.sendMessage(clr);
            }
            return false;//Bad Request, Disconnect Client
        }

        /// <summary>
        /// Handles a DeviceListRequest by returning all of the devices from the request compartmentID.
        /// Checks if the authenticated user has privileges for the compartment.
        /// </summary>
        /// <param name="msg">The DeviceListRequest message</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_DeviceListRequest(DeviceListRequest msg)
        {
            //Check if user has authenticated
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                DeviceListResponse dlr = new DeviceListResponse();
                dlr.Devices = new List<DeviceListResponse.Device>();
                //Check if user has privileges for compartment
                Compartment cmp = DB.getCompartmentById((int)msg.CompartmentID);
                if (AuthUserGroup.GetCompartments(DB).Contains(cmp))//make sure comparator is working
                {
                    foreach (Device dev in DB.getDevicesWhere("compartmentId", cmp.ID.ToString()))//What is in this list?
                    {
                        dlr.Devices.Add(dev);
                    }
                    dlr.encrypt(crypto);
                    return msgInt.sendMessage(dlr);
                }
            }
            return false;
        }

        /// <summary>
        /// Handles a AssetListRequest by returning the assets from the request compartmentID.
        /// Checks if authenticated user has privileges for the compartment.
        /// </summary>
        /// <param name="msg">The AssetListRequest message</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_AssetListRequest(AssetListRequest msg)
        {
            //Check if user has authenticated
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                AssetListResponse alr = new AssetListResponse();
                alr.Assets = new List<AssetListResponse.Asset>();
                //Check if user has privileges for compartment
                Compartment cmp = DB.getCompartmentById((int)msg.CompartmentID);
                if (AuthUserGroup.GetCompartments(DB).Contains(cmp))//Make sure comparator is working
                {
                    foreach (Asset ast in DB.getAssetsByCompartment((int)cmp.ID))
                    {
                        alr.Assets.Add(ast);
                    }
                    alr.encrypt(crypto);
                    return msgInt.sendMessage(alr);
                }               
            }
            return false;//Bad Request, Disconnect Client
        }

        /// <summary>
        /// Handles a LogListRequest by returning the logs matching the request criteria.
        /// Ensures only events that the authenticated user has privileges for are returned.
        /// </summary>
        /// <param name="msg">The LogListRequest message</param>
        /// <returns>A boolean indicating if the connection should stay alive.</returns>
        public bool handle_LogListRequest(LogListRequest msg)
        {
            //Check if user has authenticated
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                LogListResponse llr = new LogListResponse();
                llr.Events = new List<LogListResponse.Event>();
                //Generate a base list of all logs the user has access to(by compartment)
                foreach (Compartment cmp in AuthUserGroup.GetCompartments(DB))
                {
                    //Loop through each event and convert to message struct format
                    foreach(Event evt in DB.getEventsByCompartment((int)cmp.ID))
                    {
                        llr.Events.Add(evt);
                    }
                }
                
                //Narrow down list by request criteria
                foreach (LogListResponse.Event evt in llr.Events)
                {
                    //Criteria Condition Statement
                    if ((msg.AssetID != 0 && msg.AssetID != evt.ID) ||
                        (msg.CompartmentID != 0 && msg.CompartmentID != evt.CompartmentID) ||
                        (!msg.DeviceSerial.Equals(string.Empty) && !msg.DeviceSerial.Equals(evt.DeviceSerial)) ||
                        (msg.UserID != 0 && msg.UserID != evt.UserID) ||
                        (msg.LogID != 0 && msg.LogID != evt.ID))
                    {
                        llr.Events.Remove(evt);//Will this affect loop iterator?
                    }
                }
                llr.encrypt(crypto);
                return msgInt.sendMessage(llr);
            }
            return false;//Bad Request, Disconnect Client
        }
        #endregion

        #region Subscription Requests
        public bool handle_SubscribeCompartmentRequest(SubscribeCompartmentRequest msg)
        {
            //Check if user has authenticated
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                SubscribeCompartmentResponse scr = new SubscribeCompartmentResponse();
                //Check if user has privileges for compartment
                Compartment cmp = DB.getCompartmentById((int)msg.CompartmentID);
                if (AuthUserGroup.GetCompartments(DB).Contains(cmp))//Make sure comparator is working
                {
                    scr.CompartmentID = msg.CompartmentID;
                    //Authenticate compartment passcode
                    if (cmp.Passcode.Equals(msg.PassCode))
                    {
                        scr.SubscribeStatus = SubscribeCompartmentResponse.Status.APPROVED;
                        //Add subscription on server
                        if(!ServApp.CompartmentSubs.ContainsKey(msg.CompartmentID))
                            ServApp.CompartmentSubs.Add(msg.CompartmentID, new List<ServerConnection>());
                        ServApp.CompartmentSubs[msg.CompartmentID].Add(this);
                        //Add local subscription
                        if(!Subscriptions.ContainsKey(cmp))
                            Subscriptions.Add(cmp,new List<Device>());
                    }
                    else
                    {
                        scr.SubscribeStatus = SubscribeCompartmentResponse.Status.DENIED;
                    }
                    //Encrypt and send message
                    scr.encrypt(crypto);
                    return msgInt.sendMessage(scr);
                }
            }
            return false;
        }

        public bool handle_SubscribeDeviceRequest(SubscribeDeviceRequest msg)
        {
            //Check if user has authenticated
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                SubscribeDeviceResponse sdr = new SubscribeDeviceResponse();
                sdr.DeviceSerial = msg.DeviceSerial;
                //Check if user has privileges for comparment
                Device dev = (Device)DB.getDevicesWhere("serial", msg.DeviceSerial)[0];//change to correct func call
                Compartment cmp = DB.getCompartmentById((int)dev.CompartmentID);
                if (AuthUserGroup.GetCompartments(DB).Contains(cmp))//Make sure comparator is working
                {
                    sdr.SubscribeStatus = SubscribeDeviceResponse.Status.APPROVED;
                    Subscriptions[cmp].Add(dev);
                }
                else
                {
                    sdr.SubscribeStatus = SubscribeDeviceResponse.Status.DENIED;
                }
                return msgInt.sendMessage(sdr);
            }
            return false;
        }
        #endregion

        #region Misc
        public bool handle_CreateLog(CreateLogMessage msg)
        {
            //Check if user is authenticated
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                //Check if client is subscribed to compartment
                Compartment cmp = DB.getCompartmentById((int)msg.CompartmentID);
                if (Subscriptions.ContainsKey(cmp))
                {
                    //If applicable, check if client is subscribed to device
                    if(msg.DeviceID != 0 && Subscriptions[cmp].Contains(DB.getDeviceById((int)msg.DeviceID)))
                    {
                        return ServApp.DataBase.addEvent((int)msg.CompartmentID, msg.EventType, 
                            (int)msg.UserID, (int)msg.AssetID, (int)msg.DeviceID, msg.EventContent) != 0;
                    }
                }
            }
            return false;
        }

        public bool handle_EditAsset(EditAssetMessage msg)
        {
            if (AuthenticatedUser != null)
            {
                msg.decrypt(crypto);
                //Check if asset exists
                Asset ast = DB.getAssetById((int)msg.AssetID);
                if(ast != null)
                {
                    //Check if client is subscribed to asset's compartment
                    Compartment cmp = DB.getCompartmentById((int)msg.AssetID);
                    if (Subscriptions.ContainsKey(cmp))
                    {
                        Asset asset = (Asset)ServApp.DataBase.getAssetById((int)msg.AssetID);
                        asset.Name = msg.AssetName;
                        asset.CompartmentID = msg.CompartmentID;
                        asset.UserID = msg.UserID;
                        return true;
                    }
                }
            }
            return false;
        }
        #endregion
        #endregion

        #region Event Handlers
        public void SensorRecvCallback(SensorFrame frame)
        {
            DeviceCallbackMessage dcm = new DeviceCallbackMessage();
            dcm.EventTimestamp = frame.Timestamp;
            dcm.DeviceData = (byte[])frame.Data;
            dcm.DeviceSerial = ServApp.SensorDeviceMap[frame.Sender].Serial;
            dcm.EventType = DeviceCallbackMessage.DeviceEvent.VALUE_CHANGE;
            dcm.encrypt(crypto);
            msgInt.sendMessage(dcm);
        }
        #endregion
    }
}
